package com.nulldev.util.web.YouTubeExtractor.extractors.third_party;

import java.net.NoRouteToHostException;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ft.FutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.MarkedForRemoval;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor.ABIFeatures;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.formats.Formats;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.related.VideoInfo;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;

/**
 * >inb4 EEE's NOTICE: This is a work in progress and will NOT act as a primary
 * provider at all. <br>
 * <br>
 * NOTICE: Due to the primary instance of Invidious shutting down as of
 * September 2020, this extractor will be completed to around ABI 2xxx (with
 * partial support for 3xxx and higher) and then frozen, only receiving
 * emergency updates. <br>
 * <br>
 * TODO:<br>
 * Completed:<br>
 * - Fix nullptr with audio. <br>
 * - Switch to universal executor API.<br>
 * - URLs aren't processed through Invidious' internal-proxy, this is bad.
 * (partially fixed)
 */
@MarkedForRemoval(reason = "YTE is stable enough (non-agegate wise) to not require Invidious as an optional fallback", plannedRemovalWhen = "2.2.10")
public class InvidiousExtractor extends IYouTubeExtractor {

	/**
	 * This list is very likely to be outdated, good luck.
	 */
	private static final String[] INVIDIOUS_INSTANCES = new String[]
		{ "https://invidious.snopyta.org/", "https://yewtu.be/", "https://invidious.tube/", "https://invidious.kavin.rocks/", "https://tube.connect.cafe/",
				"https://invidious.zapashcanon.fr/", "https://invidiou.site/", "https://invidious.048596.xyz/", "https://vid.puffyan.us/",
				"https://invidious.namazso.eu/", "https://invidious.ethibox.fr/", "https://inv.skyn3t.in/", "https://invidious.himiko.cloud/" };

	private static final IExecutorManager td = ExecutorManager.get();
	private static final boolean TIMING_DEBUG = Arguments.hasArgument("--YouTubeNextFEERolling.timing");

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		return ArrayUtils.random(INVIDIOUS_INSTANCES) + "api/v1/videos/" + videoID; // + "?local=true";
	}

	private static final Logger logger = LoggerFactory.getLoggerD(true, true);

	private class ExtractedRequest {
		public final String result;
		public final String cookiePayload;

		public ExtractedRequest(final String result, final String cookiePayload) {
			this.result = result;
			this.cookiePayload = cookiePayload;
		}
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		return ExecutorManager.make(() -> {
			final List<YouTubeVideo> results = FastLists.list(OUTPUT_LIST_DEFAULT_SIZE);
			final variable<Integer> _index = new variable<Integer>(-1);
			final long t = System.currentTimeMillis();
			final Object _result = td.queueTask(new FutureTask<Object>() {

				@Override
				public Object execute() {
					if (TIMING_DEBUG) {
						logger.debug("generateTask::START_THREADING -> " + (System.currentTimeMillis() - t) + " ms.");
					}
					logger.debug("=> Generating URL...");
					final StringBuilder extras = new StringBuilder();
					if (arguments.containsKey(MAP_TYPE_PLAYLIST_STR)) {
						extras.append("&list=");
						extras.append(arguments.get(MAP_TYPE_PLAYLIST_STR));
					}
					if (arguments.containsKey(MAP_TYPE_PLAYLIST_INDEX)) {
						extras.append("&index=");
						final int ix = (int) arguments.get(MAP_TYPE_PLAYLIST_INDEX);
						extras.append(ix);
						_index.set((int) arguments.get(MAP_TYPE_PLAYLIST_INDEX));
					}
					final URL url = URLUtils.parseURL(generateURL(videoID, arguments) + extras);
					logger.debug("URL: " + url);
					Entry[] _args = new Entry[0];
					if (TIMING_DEBUG) {
						logger.debug("generateTask::GENERATE_URL -> " + (System.currentTimeMillis() - t) + " ms.");
					}
					String cookiePayload = "";
					try {
						logger.debug("=> Downloading metadata...");
						final HttpResponse<String> x = HttpClient.getString().url(url).timeout(Duration.ofSeconds(20)).headers(_args).create();
						for (final String cookie : x.cookies()) {
							cookiePayload += cookie.split(";")[0] + ";";
						}
						if (cookiePayload.endsWith(";")) {
							cookiePayload = cookiePayload.substring(0, cookiePayload.length() - 1);
						}
						return new ExtractedRequest(x.dataThenClose().replaceAll("\u0008", ""), cookiePayload);
					} catch (Exception e) {
						if (e instanceof NoRouteToHostException) {
							return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
									.setYTMessage("No route to youtube.com was found, this might be the internet.");
						}
						return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
								.setYTMessage("Hit the following exception: " + e.getMessage());
					}
				}
			}).waitTillCompletion();
			if (TIMING_DEBUG) {
				logger.debug("generateTask::GET_PAGE_DATA -> " + (System.currentTimeMillis() - t) + " ms.");
			}
			if (_result instanceof ExtractedVideo) {
				return ((ExtractedVideo) _result);
			}
			final String cookiePayload = ((ExtractedRequest) _result).cookiePayload;
			logger.trace("cookiePayload: " + cookiePayload);
			final String result = ((ExtractedRequest) _result).result;
			if (result.contains("To see this content, please select another instance")) {
				return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
						.setYTMessage("This instance has shutdown, sorry about that.");
			} else if (result.startsWith("<")) {
				return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
						.setYTMessage("This instance returned a HTML response.");
			} else if (result.contains("error code: 1020")) {
				return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
						.setYTMessage("This instance uses a form of JS DDOS protection.");
			}
			final UniJsonValue data = UniJsonValue.parse(result);
			if (data == null) {
				logger.info(result);
				throw new NullPointerException("data is null!");
			}
			final FutureTask<YouTubeMetadata> metadata = (FutureTask<YouTubeMetadata>) td.queueTask(new FutureTask<YouTubeMetadata>() {

				@SuppressWarnings("unused")
				@Override
				public YouTubeMetadata execute() {
					final List<YouTubeCaptions> captions = FastLists.list();
					if (data.contains("captions") && false) {
						// captions found
						final UniJsonValue ct = data.get("captions"); // FIXME
						if (ct == null || ct.isNull()) {
							logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only enabled.
						} else {
							final UniJsonIterableValue captionArray = ct.asArray();
							if (!captionArray.isNull()) {
								for (final UniJsonValue caption : captionArray) {
									final String captionURL = caption.getString("baseUrl");
									final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
									final String captionCode = caption.getString("languageCode");
									captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
								}
							}
						}
					}
					UniJsonValue keywords = UniJsonValue.array();
					try {
						keywords = data.get("keywords").asArray();
					} catch (Exception _e) {
					}
					if (keywords == null) {
						logger.warn("Fixed keywords!");
						keywords = UniJsonValue.array();
					}
					final boolean is360 = false; // FIXME: Get 360 working via projectionType
					final YouTubeMetadata metadata = new YouTubeMetadata(videoID, data.getString("title", "N/A").replaceAll(Pattern.quote("%3B"), ";"),
							data.getString("author", "N/A").replaceAll(Pattern.quote("%3B"), ";"), data.getString("authorId", "N/A"),
							data.getLong("viewCount", -1), data.getLong("lengthSeconds", -1), data.getDouble("rating", 5), -1, -1, is360, keywords.toArray(),
							captions);
					metadata.setVR180(is360); // FIXME: Proper soluton.
					metadata.limitedExtraction(result.contains("verify_controversy") || arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS));
					if (TIMING_DEBUG) {
						logger.debug("generateTask::METADATA_FINISH -> " + (System.currentTimeMillis() - t) + " ms.");
					}
					return metadata;
				}
			});
			if (data.hasValue("formatStreams")) {
				data.get("formatStreams").asArray().forEach((stream) -> {
					final int itag = MathUtil.safeParseInt(stream.getString("itag"), -1);
					if (itag == -1)
						return;
					results.add(new YouTubeVideo(stream.getString("url"), false, itag, Formats.get(itag)).setCodecMime(stream.getString("type"))
							.setFPS(stream.getInt("fps", 30)).setSize(stream.getString("size")));
				});
			}
			if (data.hasValue("adaptiveFormats")) {
				data.get("adaptiveFormats").asArray().forEach((stream) -> {
					final int itag = MathUtil.safeParseInt(stream.getString("itag"), -1);
					if (itag == -1)
						return;
					results.add(new YouTubeVideo(stream.getString("url"), true, itag, Formats.get(itag)).setCodecMime(stream.getString("type"))
							.setFPS(stream.getInt("fps", 30)).setSize(stream.getString("size", "0x" + stream.getString("resolution", "0p").replace("p", "")))
							.setFileSize(stream.getLong("clen")).setBitrate(stream.getInt("bitrate")).setInitPayload(stream.getString("init"))
							.setIndexPayload(stream.getString("index")));
				});
			}
			if (metadata.waitTillCompletion() != null) {
				final List<VideoInfo> related = FastLists.list();
				data.get("recommendedVideos").asArray().forEach((video) -> {
					related.add(new VideoInfo(video.getString("title"), video.getString("videoId"), video.getString("author"),
							String.valueOf(video.getLong("lengthSeconds")), String.valueOf(video.getLong("viewCount")))
							.setDuration(video.getLong("lengthSeconds")));
				});
				metadata.waitTillCompletion().setRelatedVideoOverride(related);
			}
			return new ExtractedVideo(results, metadata.waitTillCompletion(), ExtractionResult.SUCCESS);
		});
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

	@Override
	public String getID() {
		return "third_party.invidious";
	}

	@Override
	public int abiVersion() {
		return 2400;
	}

	public static final ABIFeatures[] FEATURES = new ABIFeatures[]
		{ ABIFeatures.VIDEO_EXTRACTION, ABIFeatures.VIDEO_BASIC_METADATA, ABIFeatures.VIDEO_RELATED_CONTENT_SUPPORT, ABIFeatures.VIDEO_CONTROVERSY_BYPASS,
				ABIFeatures.VIDEO_CAPTIONS_BASIC, ABIFeatures.VIDEO_CAPTIONS_FULL, ABIFeatures.VIDEO_LIKE_DISLIKE_RATIO, ABIFeatures.VIDEO_DECIPHERER_AUTO,
				ABIFeatures.EXTRACTOR_NO_HTML_PARSING, ABIFeatures.EXTRACTOR_V4_THREADING };

	@Override
	public ABIFeatures[] abiProvides() {
		return FEATURES;
	}

	public static void main(String[] args) throws Exception {
		final Executable<ExtractedVideo> e = new InvidiousExtractor().generateTask("QQ_3S-IQm38");
		ExecutorManager.get().queueTask(e);
		JVM.println(e.waitTillCompletion());
		JVM.println("Formats:");
		ArrayUtils.printLln(e.result().videos());
	}
}
